#include <graphics/Image.h>
#include <graphics/Engine.h>
#include <system/Log.h>

#include <FreeImage.h>

namespace ne
{
    Image::Image(ResourceManager *pManager,
                 const uint64_t id,
                 const std::string &name,
                 const std::string &fileName,
                 const bool isManual,
                 ManualResourceLoader *pLoader)
        : Resource(pManager, id, name, fileName, isManual, pLoader),
          mWidth(0), mHeight(0), mDepth(0),
          mFormat(PF_UNKNOWN),
          mPixelType(PT_UNSIGNED_BYTE),
          mpData(0)
    {
    }

    // Create Image and new memory
    void Image::createInternalImage(const uint32_t width, const uint32_t height, const uint32_t depth, const PixelFormat format, const PixelType pt)
    {
        freeInternalImage();

        mWidth = width;
        mHeight = height;
        mDepth = depth;
        mFormat = format;
        mPixelType = pt;

        mSize = _calculateSize();

        mpData = new byte[mSize];
        memset(mpData, 0, mSize);
    }

    void Image::freeInternalImage()
    {
        mWidth = 0;
        mHeight = 0;
        mDepth = 0;
        mFormat = PF_UNKNOWN;
        mPixelType = PT_UNSIGNED_BYTE;
        mSize = 0;
        delete[] mpData;
        mpData = 0;
    }

    // Load data, delete the old memory and new memory
    void Image::loadData(const void *data, const uint32_t width, const uint32_t height, const uint32_t depth, const PixelFormat format, const PixelType pt)
    {
        freeInternalImage();

        mWidth = width;
        mHeight = height;
        mDepth = depth;
        mFormat = format;
        mPixelType = pt;

        mSize = _calculateSize();

        mpData = new byte[mSize];
        memcpy(mpData, data, mSize);
    }

    uint32_t Image::getWidth() const
    {
        return mWidth;
    }

    uint32_t Image::getHeight() const
    {
        return mHeight;
    }

    uint32_t Image::getDepth() const
    {
        return mDepth;
    }

    uint32_t Image::getBPPBD() const
    {
        switch (mPixelType)
        {
            case PT_BITMAP:
                return 1;

            case PT_BYTE:
            case PT_UNSIGNED_BYTE:
                return 8;

            case PT_SHORT:
            case PT_UNSIGNED_SHORT:
                return 16;

            case PT_INT:
            case PT_UNSIGNED_INT:
            case PT_FLOAT:
                return 32;

            default:
                return 0;
        }
    }

    uint32_t Image::getBPP() const
    {
        switch (mFormat)
        {
            case PF_BITMAP:
                return 1;

            case PF_RED:
            case PF_GREEN:
            case PF_BLUE:
            case PF_ALPHA:
            case PF_LUMINANCE:
                return getBPPBD();

            case PF_LUMINANCE_ALPHA:
                return 2 * getBPPBD();

            case PF_RGB:
            case PF_BGR:
                return 3 * getBPPBD();

            case PF_RGBA:
            case PF_BGRA:
                return 4 * getBPPBD();

            default:
                return 0;
        }
    }

    uint32_t Image::getPixelSize() const
    {
        return (getBPP() + 7) >> 3;
    }

    PixelFormat Image::getFormat() const
    {
        return mFormat;
    }

    PixelType Image::getPixelType() const
    {
        return mPixelType;
    }

    Color Image::getColorAt(const uint32_t x, const uint32_t y, const uint32_t z) const
    {
        if (x < mWidth && y < mHeight && z < mDepth)
        {
            uint32_t pixelSize = getPixelSize();
            uint32_t rowSize = mWidth * pixelSize;
            uint32_t blockSize = rowSize * mHeight;
            uint32_t pos = z * blockSize + y * rowSize + x * pixelSize;
            switch (mFormat)
            {
                case PF_RED:
                    return Color(mpData[pos], 0, 0);

                case PF_GREEN:
                    return Color(0, mpData[pos], 0);

                case PF_BLUE:
                    return Color(0, 0, mpData[pos]);

                case PF_ALPHA:
                    return Color(0, 0, 0, mpData[pos]);

                case PF_LUMINANCE:
                    return Color(mpData[pos],
                                 mpData[pos],
                                 mpData[pos]);

                case PF_LUMINANCE_ALPHA:
                    return Color(mpData[pos],
                                 mpData[pos],
                                 mpData[pos],
                                 mpData[pos + 1]);

                case PF_RGB:
                    return Color(mpData[pos],
                                 mpData[pos + 1],
                                 mpData[pos + 2]);

                case PF_BGR:
                    return Color(mpData[pos + 2],
                                 mpData[pos + 1],
                                 mpData[pos]);

                case PF_RGBA:
                    return Color(mpData[pos],
                                 mpData[pos + 1],
                                 mpData[pos + 2],
                                 mpData[pos + 3]);

                case PF_BGRA:
                    return Color(mpData[pos + 2],
                                 mpData[pos + 1],
                                 mpData[pos],
                                 mpData[pos + 3]);

                default:
                    return Color(0);
            }
        }
        return Color(0);
    }

    bool Image::hasAlpha() const
    {
        if (mFormat == PF_ALPHA ||
            mFormat == PF_LUMINANCE_ALPHA ||
            mFormat == PF_RGBA ||
            mFormat == PF_BGRA)
        {
            return true;
        }
        return false;
    }

    byte* Image::getData(const uint32_t z)
    {
        if (z < mDepth)
        {
            uint32_t offset = z * getPixelSize() * mHeight * mWidth;
            return mpData + offset;
        }
        return 0;
    }

    const byte* Image::getData(const uint32_t z) const
    {
        if (z < mDepth)
        {
            uint32_t offset = z * getPixelSize() * mHeight * mWidth;
            return mpData + offset;
        }
        return 0;
    }

    Image::~Image()
    {
        unload();
    }

    bool Image::loadImplement()
    {
        FreeImage_Initialise();

        // Image format
        FREE_IMAGE_FORMAT fif = FIF_UNKNOWN;
        // Pointer to the image, once loaded
        FIBITMAP *dib(0);
        // Pointer to the image data
        BYTE *bits(0);
        // Image type
        FREE_IMAGE_TYPE imageType;
        // Image BPP(Bits Per Pixel);
        uint32_t bpp;
        // Image width, height, line
        uint32_t width(0), height(0), line(0);

        std::string fileName = getFileName();

        FILE *file = fopen(fileName.c_str(), "rb");
        if (!file)
        {
            fileName = Engine::GetSingleton().getWorkSpace() + getFileName();
            file = fopen(fileName.c_str(), "rb");
            if (!file)
            {
                Log::Out(LL_WARNING,
                    "Create Image (%s) file (%s) failed",
                    getName().c_str(), getFileName().c_str());
                return false;
            }
        }
        fclose(file);

        // Check the file signature and deduce its format
        fif = FreeImage_GetFileType(fileName.c_str(), 0);
        // If still unknown, try to guess the file format from the file extension
        if (fif == FIF_UNKNOWN)
        {
            fif = FreeImage_GetFIFFromFilename(fileName.c_str());
        }
        // If still unkown, return failure
        if (fif == FIF_UNKNOWN)
        {
            FreeImage_DeInitialise();
            Log::Out(LL_WARNING,
                "Image (%s) file (%s) with unknow format",
                getName().c_str(), getFileName().c_str());
            return false;
        }

        // Check that the plugin has reading capabilities and load the file
        if (FreeImage_FIFSupportsReading(fif))
        {
            dib = FreeImage_Load(fif, fileName.c_str());
        }
        // If the image failed to load, return failure
        if (!dib)
        {
            Log::Out(LL_WARNING,
                "Load Image (%s) from file (%s) failed",
                getName().c_str(), fileName.c_str());
            return false;
        }

        imageType = FreeImage_GetImageType(dib);
        if (imageType == FIT_UNKNOWN)
        {
            FreeImage_Unload(dib);
            FreeImage_DeInitialise();
            Log::Out(LL_WARNING,
                "Image (%s) file (%s) with unknow type",
                getName().c_str(), fileName.c_str());
            return false;
        }

        if (imageType != FIT_BITMAP)
        {
            // Convert To Standard Type
            Log::Out(LL_LOG,
                "Image (%s) file (%s) need to convert to standard type",
                getName().c_str(), fileName.c_str());
            dib = FreeImage_ConvertToStandardType(dib);
        }

        // Retrieve the image data
        bits = FreeImage_GetBits(dib);
        // Get the image width and height
	    width = FreeImage_GetWidth(dib);
	    height = FreeImage_GetHeight(dib);
	    if ((bits == 0) || (width == 0) || (height == 0))
        {
            FreeImage_Unload(dib);
            FreeImage_DeInitialise();
            Log::Out(LL_LOG,
                "Image (%s) file (%s) has no data",
                getName().c_str(), fileName.c_str());
		    return false;
        }

        PixelFormat format = PF_UNKNOWN;
        PixelType pt = PT_UNSIGNED_BYTE;
        bpp = FreeImage_GetBPP(dib);
        switch (bpp)
        {
            case 1:
                format = PF_BITMAP;
                pt = PT_BITMAP;
                break;

            case 8:
                format = PF_ALPHA;
                break;

            case 16:
                format = PF_LUMINANCE_ALPHA;
                break;

            case 24:
                #if FREEIMAGE_COLORORDER == FREEIMAGE_COLORORDER_BGR
                    format = PF_BGR;
                #else
                    format = PF_RGB;
                #endif
                break;

            case 32:
                #if FREEIMAGE_COLORORDER == FREEIMAGE_COLORORDER_BGR
                    format = PF_BGRA;
                #else
                    format = PF_RGBA;
                #endif
                break;

            default:
                FreeImage_Unload(dib);
                FreeImage_DeInitialise();
                Log::Out(LL_WARNING,
                    "Image (%s) file (%s) with unknow BitPerPixel",
                    getName().c_str(), fileName.c_str());
                return false;
        }

        createInternalImage(width, height, 1, format, pt);

        uint32_t pitch = FreeImage_GetPitch(dib);
        line = (width * bpp + 7) >> 3;
        for (uint32_t i=0; i<height; ++i)
        {
            memcpy(&mpData[i * line], &bits[i * pitch], line);
        }

        FreeImage_Unload(dib);
        FreeImage_DeInitialise();

        return true;
    }

    bool Image::unloadImplement()
    {
        freeInternalImage();
        return true;
    }

    bool Image::downloadImplement()
    {
        return true;
    }

    size_t Image::_calculateSize() const
    {
        return mHeight * mDepth * ((mWidth * getBPP() + 7) >> 3);
    }

    uint32_t Image::GetPixelSize(const PixelFormat format, const PixelType pt)
    {
        uint32_t bs = 0;
        uint32_t pixelSize = 0;
        switch (pt)
        {
            case PT_BYTE:
            case PT_UNSIGNED_BYTE:
                bs = 1;

            case PT_SHORT:
            case PT_UNSIGNED_SHORT:
                bs = 2;

            case PT_INT:
            case PT_UNSIGNED_INT:
            case PT_FLOAT:
                bs = 4;

            default:
                bs = 0;
        }

        switch (format)
        {
            case PF_RED:
            case PF_GREEN:
            case PF_BLUE:
            case PF_ALPHA:
            case PF_LUMINANCE:
                pixelSize = bs;

            case PF_LUMINANCE_ALPHA:
                pixelSize = 2 * bs;

            case PF_RGB:
            case PF_BGR:
                pixelSize = 3 * bs;

            case PF_RGBA:
            case PF_BGRA:
                pixelSize = 4 * bs;

            default:
                pixelSize = 0;
        }

        return pixelSize;
    }
}
